package de.hft_stuttgart.dsa.ctt.processing;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import de.hft_stuttgart.dsa.ctt.model.AbstractMatrix;
import de.hft_stuttgart.dsa.ctt.model.Course;
import de.hft_stuttgart.dsa.ctt.model.CourseAssignmentMatrix;
import de.hft_stuttgart.dsa.ctt.model.CourseAvailabilityMatrix;
import de.hft_stuttgart.dsa.ctt.model.CourseCompetitionMatrix;
import de.hft_stuttgart.dsa.ctt.model.Curriculum;
import de.hft_stuttgart.dsa.ctt.model.CustomCurriculumPeriodConstraint;
import de.hft_stuttgart.dsa.ctt.model.CustomTeacherPeriodConstraint;
import de.hft_stuttgart.dsa.ctt.model.MyCourse;
import de.hft_stuttgart.dsa.ctt.model.Period;
import de.hft_stuttgart.dsa.ctt.model.ProblemInstance;
import de.hft_stuttgart.dsa.ctt.model.TimeSlot;
import de.hft_stuttgart.dsa.ctt.model.UnavailabilityConstraint;
import de.hft_stuttgart.dsa.ctt.validation.FeasibleSolutionChecker;

/**
 * Should only used with the {@link CourseCompetitionMatrix}.
 * 
 * @author Matthias Huber - MatthewHuber@web.de
 */
public class TimeslotCourseAssigner implements IAssigner {

	private ProblemInstance problemInstance;
	private AbstractMatrix courseCompetitionMatrix;
	private List<MyCourse> notAssignedCourses = new ArrayList<MyCourse>();
	private CourseAssignmentMatrix courseAssignmentMatrix;

	private int assignmentCounter = 0;

	private List<CustomCurriculumPeriodConstraint> customCurriculumPeriodConstraints = new ArrayList<CustomCurriculumPeriodConstraint>();
	private List<CustomTeacherPeriodConstraint> customTeacherPeriodConstraints = new ArrayList<CustomTeacherPeriodConstraint>();

	/**
	 * @param problemInstance
	 *            The problem instance containing all necessary information
	 *            needed during the calculation steps.
	 * @param matrix
	 *            The {@link CourseAvailabilityMatrix} or the
	 *            {@link CourseCompetitionMatrix}.
	 */
	public TimeslotCourseAssigner(ProblemInstance problemInstance, AbstractMatrix matrix) {
		super();
		this.problemInstance = problemInstance;
		this.courseCompetitionMatrix = matrix;

		List<Course> courses = problemInstance.getPriorityBasedCourses();
		for (Course course : courses) {
			notAssignedCourses.add(new MyCourse(course));
		}
		courseAssignmentMatrix = new CourseAssignmentMatrix(problemInstance.getRooms(), problemInstance.getPeriods());
	}

	/**
	 * Returns a feasible Solution or null;
	 */
	@Override
	// TODO refactore the many if conditions
	public CourseAssignmentMatrix assignCoursesToMatrix() {
		boolean moreLecturesAssignable = true;
		boolean noSolution = false;
		while (moreLecturesAssignable && !notAssignedCourses.isEmpty()) {

			// first: select the next critical (lowest number). If more than one
			// than randomly
			TimeSlot nextCriticalTimeSlot = getNextCriticalTimeSlot(courseCompetitionMatrix.getPeriodRoomMatrix());

			// second: check each course if it is assignable. Choose one of them
			// randomly
			if (null != nextCriticalTimeSlot) {
				MyCourse course = getCourse(nextCriticalTimeSlot.getPeriodIndex());

				if (null != course) {
					// third: assign course to matrix
					boolean remainingLectures = assignCourseToTimeSlot(course, nextCriticalTimeSlot);

					// forth: update matrix values => return value could be a
					// bool
					// flag
					// which represents a quit for this try
					moreLecturesAssignable = updateCourseCompetitionMatrix(course, nextCriticalTimeSlot, remainingLectures);
				} else {
					// finish try
					// System.out.println("NO FEASIBLE SOLUTION WAS FOUND! COURSE");
					// moreCoursesAssignable = false;
					// noSolution = true;
				}
			} else {
				// finish try
				// System.out.println("NO FEASIBLE SOLUTION WAS FOUND! TIMESLOT");
				moreLecturesAssignable = false;
				noSolution = true;
			}

		}

		if (noSolution) {
			return null;
		}

		FeasibleSolutionChecker checker = new FeasibleSolutionChecker(courseAssignmentMatrix, problemInstance);
		boolean isFeasibleSolution = checker.checkSolution();
		if (isFeasibleSolution) {
			// System.out.println("!!! FEASIBLE SOLUTION !!!");
		} else {
			System.out.println("!!! NO FEASIBLE SOLUTION !!!");
		}
		if (isFeasibleSolution) {
			return courseAssignmentMatrix;
		} else {
			return null;
		}
	}

	/**
	 * @param periodRoomMatrix
	 * @return Returns a list with 2 values. First value defines the index of
	 *         the period, the second the room index.
	 */
	private TimeSlot getNextCriticalTimeSlot(Object[][] periodRoomMatrix) {
		int lowestAssignableLectures = Integer.MAX_VALUE;
		List<TimeSlot> mostCriticalTimeSlots = new ArrayList<TimeSlot>();

		// find the indices of the lowest assignable courses
		for (int i = 0; i < periodRoomMatrix.length; ++i) {
			for (int j = 0; j < periodRoomMatrix[i].length; ++j) {
				int timeslotValue = ((Integer) periodRoomMatrix[i][j]).intValue();
				// only handle timeslots which are free
				if (-1 != timeslotValue) {
					if (timeslotValue > lowestAssignableLectures) {
						// do nothing because this condition doesn't change
						// anything
					} else {
						// new found lowest value then reset of list is
						// necessary
						if (timeslotValue < lowestAssignableLectures) {
							mostCriticalTimeSlots.clear();
							// remember the new lowest value
							lowestAssignableLectures = timeslotValue;
						}
						// add found timeslot to lists
						mostCriticalTimeSlots.add(new TimeSlot(i, j));
					}
				}
			}
		}

		// if the list is empty then we can finish because it's not possible to
		// assign more courses to timeslots
		// TODO: maybe this assumption is wrong because the values in the
		// courseCompetitionMatrix is set to -1 too early
		// TODO: I think then its necessary to set this slot to -1 and go
		// further; quitting with the slot is not a good
		if (mostCriticalTimeSlots.isEmpty()) {
			return null;
		}

		// choose one of the found randomly
		Random generator = new Random();
		int generatedListIndex = generator.nextInt(mostCriticalTimeSlots.size());

		return mostCriticalTimeSlots.get(generatedListIndex);
	}

	/**
	 * @param period
	 * @return The course or null if no course is assignable to that timeslot.
	 */
	private MyCourse getCourse(int period) {
		List<MyCourse> assignableCourses = new ArrayList<MyCourse>();
		for (MyCourse myCourse : notAssignedCourses) {
			if (isCourseAssignableToTimeslot(myCourse, period)) {
				assignableCourses.add(myCourse);
			}
		}

		// if the list is empty then we can finish because it's not possible to
		// assign a course anymore
		// TODO: maybe this assumption is wrong and remaining courses can be
		// assigned to other timeslots => could be done by setting the value of
		// the timeslot to -1
		// TODO: I think then its necessary to set the slot to -1 and go
		// further; quitting with the course is not a good idea !!!!
		if (assignableCourses.isEmpty()) {
			Object[][] matrix = courseCompetitionMatrix.getPeriodRoomMatrix();
			for (int i = 0; i < matrix[period].length; ++i) {
				matrix[period][i] = -1;
			}
			return null;
		} else {
			List<Integer> rangeList = new ArrayList<Integer>(assignableCourses.size());
			// used to set up a ranking for the courses. Example: Course 1 has a
			// priority of 10, Course 2 has a priority of 90.
			// We add int = 10 to the list and int = 100 (10 +90) to the list.
			// So
			// afterwards the possibility to choose Course 1 is 10%. The
			// ordering of
			// the list reflects the ordering of courses in the
			// assignableCourses
			// list.
			int aggregatedPriorityPoints = 0;
			for (MyCourse myCourse : assignableCourses) {
				Course course = myCourse.getCourse();
				// based on the problem instance
				aggregatedPriorityPoints += course.getPriority();
				// based on the progress
				// aggregatedPriorityPoints += myCourse.getPriority();
				rangeList.add(aggregatedPriorityPoints);
			}

			// choose a random course
			Random generator = new Random();
			// System.out.println(aggregatedPriorityPoints);
			int calculatedPriorityPoint = generator.nextInt(aggregatedPriorityPoints) + 1;

			MyCourse course = null;
			int listIndex = 0;
			while (listIndex < rangeList.size()) {
				// we have found the course if calculatedPriorityPoints is less
				// or equal the aggregatedPoint. Previous Example: if the value
				// is
				// 1 to 10 the course is Course one otherwise Course 2;
				if (calculatedPriorityPoint <= rangeList.get(listIndex)) {
					course = assignableCourses.get(listIndex);
					break;
				}
				++listIndex;
			}

			if (null == course) {
				new RuntimeException("What the hell happened. There should be at least one remaining course for that timeslot.");
			}
			return course;
		}
	}

	private boolean isCourseAssignableToTimeslot(MyCourse myCourse, int period) {
		Course course = myCourse.getCourse();
		for (UnavailabilityConstraint constraint : course.getUnavailabilityConstraints()) {
			// calculate the period of the constraint
			int unavailabilityPeriod = Period.getCalculatedPeriod(problemInstance.getPeriodsPerDay(), constraint.getPeriodOfDay(), constraint.getDayOfWeek());

			// if constraint period is equals to actual period then this course
			// is not assignable
			if (period == unavailabilityPeriod) {
				return false;
			}
		}

		for (CustomCurriculumPeriodConstraint constraint : customCurriculumPeriodConstraints) {
			if (period == constraint.getPeriod()) {
				List<Curriculum> curriculums = course.getCurriculums();
				for (Curriculum curriculum : curriculums) {
					if (curriculum.getCurriculumId().equals(constraint.getCurriculum())) {
						return false;
					}
				}
			}
		}

		for (CustomTeacherPeriodConstraint constraint : customTeacherPeriodConstraints) {
			if (period == constraint.getPeriod()) {
				if (course.getTeacher().equals(constraint.getTeacher())) {
					return false;
				}
			}
		}

		return true;
	}

	private boolean assignCourseToTimeSlot(MyCourse myCourse, TimeSlot timeslot) {
		int periodIndex = timeslot.getPeriodIndex();
		int roomIndex = timeslot.getRoomIndex();
		Course[][] courseMatrix = (Course[][]) courseAssignmentMatrix.getPeriodRoomMatrix();

		// check if a course is already assigned to this timeslot
		Course cell = courseMatrix[periodIndex][roomIndex];
		if (null != cell.getCourseId()) {
			throw new RuntimeException("What the hell happened. There should be no course assigned");
		}

		// assign course
		Course course = (Course) myCourse.getCourse();
		courseMatrix[periodIndex][roomIndex] = course;
		++assignmentCounter;
		// System.out.println(assignmentCounter + ". COURSE WAS ASSIGNED");

		// create a teacher constraint because another lecture of this teacher
		// cannot be assigned to this period
		CustomTeacherPeriodConstraint teacherConstraint = new CustomTeacherPeriodConstraint(course.getTeacher(), periodIndex);
		customTeacherPeriodConstraints.add(teacherConstraint);

		// create curriculum constraints because another lecture of the same
		// curriculum cannot be assigned to this period
		for (Curriculum curriculum : course.getCurriculums()) {
			CustomCurriculumPeriodConstraint curriculumConstraint = new CustomCurriculumPeriodConstraint(periodIndex, curriculum.getCurriculumId());
			customCurriculumPeriodConstraints.add(curriculumConstraint);
		}

		// remove course from the not assigned courses
		boolean remainingLectures = myCourse.decreaseRemainingNumberOfLectures();
		if (!remainingLectures) {
			notAssignedCourses.remove(myCourse);
			// System.out.println("Removed one course");
		}

		return remainingLectures;
	}

	/**
	 * @param timeslot
	 * @return True if for every timeslot there is at least one course
	 *         assignable otherwise false.
	 */
	private boolean updateCourseCompetitionMatrix(MyCourse myCourse, TimeSlot timeslot, boolean remainingLectures) {
		boolean furtherProgressPossible = true;

		int periodIndex = timeslot.getPeriodIndex();
		int roomIndex = timeslot.getRoomIndex();

		// set the value within the periodRoomMatrix to -1 meaning that
		// this slot is now not available anymore
		Object[][] courseMatrix = courseCompetitionMatrix.getPeriodRoomMatrix();
		courseMatrix[periodIndex][roomIndex] = -1;

		// also update the other cells of this period. Therefore decrease with
		// the left number of lectures.
		for (int i = 0; i < courseMatrix[periodIndex].length; ++i) {
			int currentValue = ((Integer) courseMatrix[periodIndex][i]).intValue();
			if (-1 != currentValue) {
				currentValue -= 1;
				if (0 != currentValue) {
					courseMatrix[periodIndex][i] = currentValue;
				} else {
					courseMatrix[periodIndex][i] = -1;
				}
			}
		}

		// decrease all cell values (more precisely all cells that are in the
		// other periods) by one if the all lectures of the course are
		// assigned and the course was there also
		// assignable; the period the course was assigned to will not be
		// affected twice because we added Constraints for teacher and
		// curriculum before
		if (!remainingLectures) {
			for (int i = 0; i < courseMatrix.length; ++i) {
				if (isCourseAssignableToTimeslot(myCourse, i)) {
					for (int j = 0; j < courseMatrix[i].length; ++j) {
						int currentValue = ((Integer) courseMatrix[i][j]).intValue();
						if (-1 != currentValue) {
							currentValue -= 1;
							if (0 != currentValue) {
								courseMatrix[i][j] = currentValue;
							} else {
								courseMatrix[i][j] = -1;
							}
						}
					}
				}
			}
		}

		return furtherProgressPossible;
	}

	public int getAssignmentCounter() {
		return assignmentCounter;
	}

}
